]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
shell: Add device monitor for cameras
authorrobert.mader@collabora.com <robert.mader@collabora.com>
Sun, 9 Jul 2023 19:14:25 +0000 (21:14 +0200)
committerrobert.mader@collabora.com <robert.mader@collabora.com>
Sat, 29 Jul 2023 10:55:15 +0000 (13:55 +0300)
Pipewire allows us to easily track whether any cameras are in used by
checking the state of camera nodes. Add a simple camera monitor to the
shell, allowing us to show e.g. a status indicator.

Naturally the monitor is limited to apps using Pipewire for camera
access and thus subject to the same chicken-egg problem like the camera
portal - it could confuse users that apps may use the camera without
being noticed by the monitor. The hope and assumption here is that a
better shell integration might speed up adoption of the new camera APIs

Pipewire 0.3.49 is required for refcounted `pw_init()`/`pw_deinit()`.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2840>

config.h.meson
meson.build
meson_options.txt
src/meson.build
src/shell-camera-monitor.c [new file with mode: 0644]
src/shell-camera-monitor.h [new file with mode: 0644]

index 928a758ae27c6320091c73e3c91f51ff859ec1d5..5ad68d8395040070904b479412689216c00496e2 100644 (file)
@@ -22,6 +22,9 @@
 /* Define if we have NetworkManager */
 #mesondefine HAVE_NETWORKMANAGER
 
+/* Define if we have Pipewire */
+#mesondefine HAVE_PIPEWIRE
+
 /* Define if we have systemd */
 #mesondefine HAVE_SYSTEMD
 
index a7eeaf4e6857227df4e8b385176af040f0f6c668..b6bfc58da4c0723d9253cf32347bb11169bfa726 100644 (file)
@@ -32,6 +32,7 @@ startup_req = '>= 0.11'
 systemd_req = '>= 246'
 ibus_req = '>= 1.5.19'
 gnome_desktop_req = '>= 40'
+pipewire_req = '>= 0.3.49'
 
 nm_req = '>= 1.10.4'
 secret_req = '>= 0.18'
@@ -102,6 +103,14 @@ else
   have_networkmanager = false
 endif
 
+if get_option('camera_monitor')
+  libpipewire_dep = dependency('libpipewire-0.3', version: pipewire_req)
+  have_pipewire = true
+else
+  libpipewire_dep = []
+  have_pipewire = false
+endif
+
 if get_option('systemd')
   libsystemd_dep = dependency('libsystemd', version: systemd_req)
   systemd_dep = dependency('systemd', version: systemd_req)
@@ -136,6 +145,7 @@ cdata.set_quoted('VERSION', meson.project_version())
 cdata.set_quoted('PACKAGE_VERSION', meson.project_version())
 
 cdata.set('HAVE_NETWORKMANAGER', have_networkmanager)
+cdata.set('HAVE_PIPEWIRE', have_pipewire)
 cdata.set('HAVE_SYSTEMD', have_systemd)
 
 # New API added in glib-2.57.2
@@ -311,6 +321,7 @@ meson.add_dist_script('meson/generate-stylesheets.py')
 meson.add_dist_script('meson/check-version.py', meson.project_version(), 'NEWS')
 
 summary_options = {
+  'camera_monitor': get_option('camera_monitor'),
   'networkmanager': get_option('networkmanager'),
   'systemd': get_option('systemd'),
   'extensions_app': get_option('extensions_app'),
index 2d317e602fce9f30340825a766920f83a612cb6f..6e83d92f2efd6641ad6bb1a65186a4822b7c46d8 100644 (file)
@@ -1,3 +1,9 @@
+option('camera_monitor',
+  type: 'boolean',
+  value: true,
+  description: 'Enable the camera monitor'
+)
+
 option('extensions_tool',
   type: 'boolean',
   value: true,
index 7ede16f451b57db5141c1d02f6c67c5f51191697..3e18aad9363a1ca65d97cb518de5c03e69f4286f 100644 (file)
@@ -63,7 +63,8 @@ gnome_shell_deps = [
   gi_dep,
   polkit_dep,
   gcr_dep,
-  libsystemd_dep
+  libsystemd_dep,
+  libpipewire_dep,
 ]
 
 gnome_shell_deps += nm_deps
@@ -97,6 +98,7 @@ libshell_public_headers = [
   'shell-app-system.h',
   'shell-app-usage.h',
   'shell-blur-effect.h',
+  'shell-camera-monitor.h',
   'shell-glsl-effect.h',
   'shell-global.h',
   'shell-invert-lightness-effect.h',
@@ -136,6 +138,7 @@ libshell_sources = [
   'shell-app-system.c',
   'shell-app-usage.c',
   'shell-blur-effect.c',
+  'shell-camera-monitor.c',
   'shell-global.c',
   'shell-glsl-effect.c',
   'shell-invert-lightness-effect.c',
diff --git a/src/shell-camera-monitor.c b/src/shell-camera-monitor.c
new file mode 100644 (file)
index 0000000..34bf2ee
--- /dev/null
@@ -0,0 +1,435 @@
+/*
+ * Shell camera monitor
+ *
+ * Copyright (C) 2023 Collabora Ltd.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include "shell-camera-monitor.h"
+
+#ifdef HAVE_PIPEWIRE
+#include <pipewire/pipewire.h>
+#include <spa/utils/result.h>
+#endif
+
+#define RECONNECT_DELAY_MS 5000
+
+enum {
+  PROP_0,
+  PROP_CAMERAS_IN_USE,
+  PROP_LAST
+};
+
+static GParamSpec *obj_props[PROP_LAST];
+
+struct _ShellCameraMonitor {
+  GObject parent;
+
+  gboolean cameras_in_use;
+
+#ifdef HAVE_PIPEWIRE
+  GPtrArray *node_list;
+  guint reconnect_id;
+
+  GSource *pipewire_source;
+  struct pw_context *context;
+  struct pw_core *core;
+  struct pw_registry *registry;
+  struct spa_hook core_listener;
+  struct spa_hook registry_listener;
+#endif
+};
+
+G_DEFINE_TYPE (ShellCameraMonitor, shell_camera_monitor, G_TYPE_OBJECT);
+
+#ifdef HAVE_PIPEWIRE
+typedef struct _Node {
+  ShellCameraMonitor *monitor;
+  gboolean running;
+  struct spa_hook proxy_listener;
+  struct spa_hook object_listener;
+} Node;
+
+typedef struct _MetaPipeWireSource
+{
+  GSource source;
+
+  struct pw_loop *pipewire_loop;
+} MetaPipeWireSource;
+
+static gboolean shell_camera_monitor_connect_core (ShellCameraMonitor *monitor);
+static void shell_camera_monitor_disconnect_core (ShellCameraMonitor *monitor);
+
+static void
+shell_camera_monitor_update_state (ShellCameraMonitor *monitor)
+{
+  gboolean new_cameras_in_use = FALSE;
+  int i;
+
+  for (i = 0; i < monitor->node_list->len; i++)
+    {
+      struct pw_proxy *proxy;
+      Node *node;
+
+      proxy = g_ptr_array_index (monitor->node_list, i);
+      node = pw_proxy_get_user_data (proxy);
+
+      if (node->running)
+        {
+          new_cameras_in_use = TRUE;
+          break;
+        }
+    }
+
+  if (new_cameras_in_use != monitor->cameras_in_use)
+    {
+      monitor->cameras_in_use = new_cameras_in_use;
+      g_object_notify_by_pspec (G_OBJECT (monitor),
+                                obj_props[PROP_CAMERAS_IN_USE]);
+    }
+}
+
+static void
+proxy_destroy (void *data)
+{
+  Node *node = data;
+
+  spa_hook_remove (&node->proxy_listener);
+  spa_hook_remove (&node->object_listener);
+}
+
+static const struct pw_proxy_events proxy_events = {
+  PW_VERSION_PROXY_EVENTS,
+  .destroy = proxy_destroy,
+};
+
+static void
+node_event_info (void                      *data,
+                 const struct pw_node_info *info)
+{
+  Node *node = data;
+
+  node->running = (info->state == PW_NODE_STATE_RUNNING ||
+                   info->state == PW_NODE_STATE_IDLE);
+
+  shell_camera_monitor_update_state (node->monitor);
+}
+
+static const struct pw_node_events node_events = {
+  PW_VERSION_NODE_EVENTS,
+  .info = node_event_info,
+};
+
+static void
+registry_event_global (void                  *data,
+                       uint32_t               id,
+                       uint32_t               permissions,
+                       const char            *type,
+                       uint32_t               version,
+                       const struct spa_dict *props)
+{
+  ShellCameraMonitor *monitor = SHELL_CAMERA_MONITOR (data);
+  struct pw_proxy *proxy;
+  const char *prop_str;
+  Node *node;
+
+  if (!props || !(spa_streq (type, PW_TYPE_INTERFACE_Node)))
+    return;
+
+  prop_str = spa_dict_lookup (props, PW_KEY_MEDIA_ROLE);
+  if (!prop_str || (strcmp (prop_str, "Camera") != 0))
+    return;
+
+  proxy = pw_registry_bind (monitor->registry,
+                            id,
+                            PW_TYPE_INTERFACE_Node,
+                            PW_VERSION_NODE,
+                            sizeof (Node));
+  node = pw_proxy_get_user_data (proxy);
+  node->monitor = monitor;
+
+  pw_proxy_add_listener (proxy,
+                         &node->proxy_listener,
+                         &proxy_events,
+                         node);
+
+  pw_proxy_add_object_listener (proxy,
+                                &node->object_listener,
+                                &node_events,
+                                node);
+
+  g_ptr_array_add (monitor->node_list, proxy);
+}
+
+static void
+registry_event_global_remove (void     *data,
+                              uint32_t  id)
+{
+  ShellCameraMonitor *monitor = SHELL_CAMERA_MONITOR (data);
+  struct pw_proxy *proxy_to_remove = NULL;
+  int i;
+
+  for (i = 0; i < monitor->node_list->len; i++)
+    {
+      struct pw_proxy *proxy;
+
+      proxy = g_ptr_array_index (monitor->node_list, i);
+      if (pw_proxy_get_bound_id (proxy) == id)
+        {
+          proxy_to_remove = proxy;
+          break;
+        }
+    }
+
+  if (proxy_to_remove)
+    g_ptr_array_remove (monitor->node_list, proxy_to_remove);
+}
+
+static const struct pw_registry_events registry_events = {
+  PW_VERSION_REGISTRY_EVENTS,
+  .global = registry_event_global,
+  .global_remove = registry_event_global_remove,
+};
+
+static void
+idle_reconnect (gpointer data)
+{
+  ShellCameraMonitor *monitor = SHELL_CAMERA_MONITOR (data);
+
+  if (shell_camera_monitor_connect_core (monitor))
+    monitor->reconnect_id = 0;
+  else
+    monitor->reconnect_id =
+      g_timeout_add_once (RECONNECT_DELAY_MS, idle_reconnect, monitor);
+}
+
+static void
+on_core_error (void       *data,
+               uint32_t    id,
+               int         seq,
+               int         res,
+               const char *message)
+{
+  if (id == 0 && res == -EPIPE)
+    {
+      ShellCameraMonitor *monitor = SHELL_CAMERA_MONITOR (data);
+
+      shell_camera_monitor_disconnect_core (monitor);
+      if (monitor->cameras_in_use)
+        {
+          monitor->cameras_in_use = FALSE;
+          g_object_notify_by_pspec (G_OBJECT (monitor),
+                                    obj_props[PROP_CAMERAS_IN_USE]);
+        }
+
+      if (monitor->reconnect_id == 0)
+        monitor->reconnect_id =
+          g_timeout_add_once (RECONNECT_DELAY_MS, idle_reconnect, monitor);
+    }
+}
+
+static const struct pw_core_events core_events = {
+  PW_VERSION_CORE_EVENTS,
+  .error = on_core_error,
+};
+
+static gboolean
+shell_camera_monitor_connect_core (ShellCameraMonitor *monitor)
+{
+  monitor->core = pw_context_connect (monitor->context, NULL, 0);
+  if (!monitor->core)
+    return FALSE;
+
+  pw_core_add_listener (monitor->core,
+                        &monitor->core_listener,
+                        &core_events,
+                        monitor);
+
+  monitor->registry = pw_core_get_registry (monitor->core,
+                                            PW_VERSION_REGISTRY,
+                                            0);
+  pw_registry_add_listener (monitor->registry,
+                            &monitor->registry_listener,
+                            &registry_events,
+                            monitor);
+  return TRUE;
+}
+
+static void
+shell_camera_monitor_disconnect_core (ShellCameraMonitor *monitor)
+{
+  g_ptr_array_set_size (monitor->node_list, 0);
+
+  g_clear_pointer ((struct pw_proxy**) &monitor->registry, pw_proxy_destroy);
+  spa_hook_remove (&monitor->registry_listener);
+  g_clear_pointer (&monitor->core, pw_core_disconnect);
+  spa_hook_remove (&monitor->core_listener);
+}
+
+static gboolean
+pipewire_loop_source_prepare (GSource *source,
+                              int     *timeout)
+{
+  *timeout = -1;
+  return FALSE;
+}
+
+static gboolean
+pipewire_loop_source_dispatch (GSource     *source,
+                               GSourceFunc  callback,
+                               gpointer     user_data)
+{
+  MetaPipeWireSource *pipewire_source = (MetaPipeWireSource *) source;
+  int result;
+
+  result = pw_loop_iterate (pipewire_source->pipewire_loop, 0);
+  if (result < 0)
+    g_warning ("pipewire_loop_iterate failed: %s", spa_strerror (result));
+
+  return TRUE;
+}
+
+static void
+pipewire_loop_source_finalize (GSource *source)
+{
+  MetaPipeWireSource *pipewire_source = (MetaPipeWireSource *) source;
+
+  pw_loop_leave (pipewire_source->pipewire_loop);
+  pw_loop_destroy (pipewire_source->pipewire_loop);
+}
+
+static GSourceFuncs pipewire_source_funcs =
+{
+  pipewire_loop_source_prepare,
+  NULL,
+  pipewire_loop_source_dispatch,
+  pipewire_loop_source_finalize
+};
+
+static GSource *
+create_pipewire_source (struct pw_loop *pipewire_loop)
+{
+  GSource *source;
+  MetaPipeWireSource *pipewire_source;
+
+  source = g_source_new (&pipewire_source_funcs, sizeof (MetaPipeWireSource));
+  g_source_set_name (source, "[gnome-shell] PipeWire");
+
+  pipewire_source = (MetaPipeWireSource *) source;
+  pipewire_source->pipewire_loop = pipewire_loop;
+
+  g_source_add_unix_fd (source,
+                        pw_loop_get_fd (pipewire_source->pipewire_loop),
+                        G_IO_IN | G_IO_ERR);
+
+  pw_loop_enter (pipewire_source->pipewire_loop);
+  g_source_attach (source, NULL);
+  g_source_unref (source);
+
+  return source;
+}
+
+#endif
+
+static void
+shell_camera_monitor_finalize (GObject *object)
+{
+#ifdef HAVE_PIPEWIRE
+  ShellCameraMonitor *monitor = SHELL_CAMERA_MONITOR (object);
+
+  shell_camera_monitor_disconnect_core (monitor);
+  g_clear_pointer (&monitor->node_list, g_ptr_array_unref);
+  g_clear_pointer (&monitor->context, pw_context_destroy);
+  g_clear_pointer (&monitor->pipewire_source, g_source_destroy);
+  g_clear_handle_id (&monitor->reconnect_id, g_source_remove);
+  pw_deinit ();
+#endif
+
+  G_OBJECT_CLASS (shell_camera_monitor_parent_class)->finalize (object);
+}
+
+static void
+shell_camera_monitor_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  ShellCameraMonitor *monitor = SHELL_CAMERA_MONITOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_CAMERAS_IN_USE:
+      g_value_set_boolean (value, monitor->cameras_in_use);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+shell_camera_monitor_class_init (ShellCameraMonitorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = shell_camera_monitor_finalize;
+  object_class->get_property = shell_camera_monitor_get_property;
+
+  obj_props[PROP_CAMERAS_IN_USE] =
+    g_param_spec_boolean ("cameras-in-use",
+                          "Cameras in use",
+                          "Whether any camera is currently used by an app",
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, PROP_LAST, obj_props);
+}
+
+static void
+shell_camera_monitor_init (ShellCameraMonitor *monitor)
+{
+#ifdef HAVE_PIPEWIRE
+  struct pw_loop *pipewire_loop;
+
+  monitor->node_list =
+    g_ptr_array_new_full (5, (GDestroyNotify) pw_proxy_destroy);
+
+  pw_init (NULL, NULL);
+
+  pipewire_loop = pw_loop_new (NULL);
+  if (!pipewire_loop)
+    goto error;
+
+  monitor->pipewire_source = create_pipewire_source (pipewire_loop);
+  if (!monitor->pipewire_source)
+    goto error;
+
+  monitor->context = pw_context_new (pipewire_loop, NULL, 0);
+  if (!monitor->context)
+    goto error;
+
+  if (!shell_camera_monitor_connect_core (monitor))
+    goto error;
+
+  return;
+
+error:
+    g_message ("Failed to start camera monitor");
+#endif
+}
diff --git a/src/shell-camera-monitor.h b/src/shell-camera-monitor.h
new file mode 100644 (file)
index 0000000..9f98116
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Shell camera monitor
+ *
+ * Copyright (C) 2023 Collabora Ltd.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+#ifndef __SHELL_CAMERA_MONITOR_H__
+#define __SHELL_CAMERA_MONITOR_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define SHELL_TYPE_CAMERA_MONITOR (shell_camera_monitor_get_type ())
+G_DECLARE_FINAL_TYPE (ShellCameraMonitor, shell_camera_monitor,
+                      SHELL, CAMERA_MONITOR, GObject)
+
+G_END_DECLS
+
+#endif /* __SHELL_CAMERA_MONITOR_H__ */